package xyz.lwm.lazycat.beans;

import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.lwm.lazycat.annotation.Destroy;
import xyz.lwm.lazycat.annotation.Init;
import xyz.lwm.lazycat.utility.Assert;
import xyz.lwm.lazycat.utility.Pair;
import xyz.lwm.lazycat.utility.reflect.AnnotationUtil;
import xyz.lwm.lazycat.utility.reflect.Annotations;
import xyz.lwm.lazycat.utility.reflect.ReflectUtil;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * bean post processor chain
 *
 * @author lwm
 */
class BeanProcessor {

    // pair of order and beanPostProcessor
    private final List<Pair<Integer, BeanPostProcessor>> beanPostProcessors = new ArrayList<>();

    @Getter
    private boolean sorted;

    // sort from smallest to biggest
    public void add(int order, @NotNull BeanPostProcessor beanPostProcessor) {
        Assert.notNull(beanPostProcessor, "beanPostProcessor");
        beanPostProcessors.add(Pair.of(order, beanPostProcessor));
    }

    // sort by order, smallest first
    public void sort() {
        beanPostProcessors.sort(Comparator.comparingInt(Pair::getLeft));
        this.sorted = true;
    }

    public Object process(Object bean, String beanName) {
        if (bean == null) {
            return null;
        }
        bean = processBeforeInitialization(bean, beanName);
        initializeBean(bean);
        bean = processAfterInitialization(bean, beanName);
        return bean;
    }

    private Object processBeforeInitialization(Object bean, String beanName) throws BeansException {
        for (Pair<Integer, BeanPostProcessor> beanPostProcessor : beanPostProcessors) {
            bean = beanPostProcessor.getRight().processBeforeInitialization(bean, beanName);
        }
        return bean;
    }

    private void initializeBean(Object bean) {
        invokePostConstruct(bean);
        invokeBeanLifecyclePost(bean);
    }

    private void invokePostConstruct(Object bean) {
        // get @PostConstruct methods
        Class<?> beanClass = bean.getClass();
        Map<String, Method> methods = ReflectUtil.findDeclaredMethods(beanClass, null, method -> {
            int modifiers = method.getModifiers();
            Annotations annotations = AnnotationUtil.getAnnotations(method);
            return Modifier.isPublic(modifiers) && method.getParameterCount() == 0 &&
                    (annotations.hasAnnotation(PostConstruct.class) ||
                            annotations.hasAnnotation(Init.class));
        }, null, BeanProcessor.class.getName() + "#initializeBean:" + beanClass.getName());
        // invoke @PostConstruct methods
        methods.values().forEach(m -> ReflectUtil.invoke(bean, m));
    }

    private void invokeBeanLifecyclePost(Object bean) {
        if (bean instanceof BeanLifecycle) {
            ((BeanLifecycle) bean).postConstruct();
        }
    }

    private Object processAfterInitialization(Object bean, String beanName) throws BeansException {
        for (Pair<Integer, BeanPostProcessor> beanPostProcessor : beanPostProcessors) {
            bean = beanPostProcessor.getRight().processAfterInitialization(bean, beanName);
        }
        return bean;
    }

    public void preDestroy(Object bean) {
        if (bean == null) {
            return;
        }

        // get @PreDestroy methods
        Class<?> beanClass = bean.getClass();
        Map<String, Method> methods = ReflectUtil.findDeclaredMethods(beanClass, null, method -> {
            int modifiers = method.getModifiers();
            Annotations annotations = AnnotationUtil.getAnnotations(method);
            return Modifier.isPublic(modifiers) && method.getParameterCount() == 0 &&
                    (annotations.hasAnnotation(PreDestroy.class) ||
                            annotations.hasAnnotation(Destroy.class));
        }, null, BeanProcessor.class.getName() + "#preDestroy:" + beanClass.getName());
        // invoke @PostConstruct methods
        methods.values().forEach(m -> ReflectUtil.invoke(bean, m));

        // BeanLifecycle#preDestroy
        if (bean instanceof BeanLifecycle) {
            ((BeanLifecycle) bean).preDestroy();
        }
    }

}
